package cn.sskxyz.mongodb.multi;

import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.domain.EntityScanner;
import org.springframework.boot.autoconfigure.mongo.MongoClientFactory;
import org.springframework.boot.autoconfigure.mongo.MongoClientSettingsBuilderCustomizer;
import org.springframework.boot.autoconfigure.mongo.MongoProperties;
import org.springframework.boot.autoconfigure.mongo.MongoPropertiesClientSettingsBuilderCustomizer;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.data.annotation.Persistent;
import org.springframework.data.mapping.model.FieldNamingStrategy;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.convert.DbRefResolver;
import org.springframework.data.mongodb.core.convert.DefaultDbRefResolver;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.convert.MongoCustomConversions;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;

import com.mongodb.MongoClientSettings;
import com.mongodb.client.MongoClient;

import cn.sskxyz.mongodb.multi.MultiMongoDatabaseFactory.MongoDatabaseFactory;
import cn.sskxyz.mongodb.multi.dynamic.DummyDatabaseProvider;
import cn.sskxyz.mongodb.multi.dynamic.DynamicDatabaseProvider;

@Configuration
@ConfigurationProperties(prefix = "system.mongodb")
public class MongoConfiguration {

	private Map<String, MongoProperties> multi;
	
	private String lockdb;

	private String primary;

	private boolean withTypeMapper = true;

	public Map<String, MongoProperties> getMulti() {
		return multi;
	}

	public void setMulti(Map<String, MongoProperties> multi) {
		this.multi = multi;
	}

	public String getPrimary() {
		return primary;
	}

	public void setPrimary(String primary) {
		this.primary = primary;
	}
	
	public String getLockdb() {
		return lockdb;
	}

	public void setLockdb(String lockdb) {
		this.lockdb = lockdb;
	}

	public boolean isWithTypeMapper() {
		return withTypeMapper;
	}

	public void setWithTypeMapper(boolean withTypeMapper) {
		this.withTypeMapper = withTypeMapper;
	}

	@Bean
	public Map<String, MongoClient> mongoClientMap(
			ObjectProvider<MongoClientSettingsBuilderCustomizer> builderCustomizers, Environment environment) {

		Map<String, MongoClient> result = new HashMap<>();

		multi.entrySet().stream().forEach(entry -> {
			MongoClientSettings settings = MongoClientSettings.builder().build();

			List<MongoClientSettingsBuilderCustomizer> custormizer = builderCustomizers.orderedStream()
					.collect(Collectors.toList());

			custormizer.add(new MongoPropertiesClientSettingsBuilderCustomizer(entry.getValue(), environment));

			MongoClient client = new MongoClientFactory(custormizer).createMongoClient(settings);

			result.put(entry.getKey(), client);
		});

		return result;
	}

	@Bean
	@ConditionalOnMissingBean(DynamicDatabaseProvider.class)
	public DynamicDatabaseProvider dynamicDatabaseProvider() {
		return new DummyDatabaseProvider("test");
	}
	
	@Bean
	public MultiMongoDatabaseFactory databaseFactory(@Qualifier("mongoClientMap") Map<String, MongoClient> mongoClientMap, 
			DynamicDatabaseProvider provider) {

		Map<String, MongoDatabaseFactory> factories = new HashMap<>();

		MultiMongoDatabaseFactory result = new MultiMongoDatabaseFactory(mongoClientMap.get(primary),
				multi.get(primary).getMongoClientDatabase(), true, factories, primary);

		mongoClientMap.forEach((key, value) -> {
			String database = multi.get(key).getMongoClientDatabase();
			if ("#".equals(database)) {
				MongoDatabaseFactory factory = result.new DynamicMongoDatabaseFactory(mongoClientMap.get(key), database,
						true, provider);
				factories.put(key, factory);
			} else {
				MongoDatabaseFactory factory = result.new MongoDatabaseFactory(mongoClientMap.get(key), database, true);
				factories.put(key, factory);
			}
		});

		return result;
	}
	
	@Bean
	public MongoCustomConversions mongoCustomConversions(List<MongoConverter> converters) {
		if(converters == null || converters.size() == 0) {
			converters = Collections.emptyList();
		}
		return new MongoCustomConversions(converters);
	}

	@Bean
	public MappingMongoConverter mappingMongoConverter(org.springframework.data.mongodb.MongoDatabaseFactory factory, MongoMappingContext context,
	MongoCustomConversions conversions) {
		DbRefResolver dbRefResolver = new DefaultDbRefResolver(factory);
		MappingMongoConverter mappingMongoConverter = new MappingMongoConverter(dbRefResolver, context);
		mappingMongoConverter.setCustomConversions(conversions);
		if(!withTypeMapper) {
			mappingMongoConverter.setTypeMapper(new DummyMongoTypeMapper());
		}
		return mappingMongoConverter;
	}

	@Bean
	public MultiMongoMappingContext mongoMappingContext(ApplicationContext applicationContext,
			MongoCustomConversions conversions) throws ClassNotFoundException {

		PropertyMapper mapper = PropertyMapper.get().alwaysApplyingWhenNonNull();

		Map<String, MongoMappingContext> mappingContextMap = new HashMap<>();

		Set<Class<?>> classes = new EntityScanner(applicationContext).scan(Document.class, Persistent.class);

		multi.forEach((key, properties) -> {
			MongoMappingContext context = new MongoMappingContext();
			mapper.from(properties.isAutoIndexCreation()).to(context::setAutoIndexCreation);
			context.setInitialEntitySet(classes);

			Class<?> strategyClass = properties.getFieldNamingStrategy();
			if (strategyClass != null) {
				context.setFieldNamingStrategy((FieldNamingStrategy) BeanUtils.instantiateClass(strategyClass));
			}

			context.setSimpleTypeHolder(conversions.getSimpleTypeHolder());
			mappingContextMap.put(key, context);
		});

		MongoMappingContext primaryContext = mappingContextMap.get(primary);

		MultiMongoMappingContext mappingContext = new MultiMongoMappingContext(primaryContext, mappingContextMap);

		return mappingContext;
	}

	@Bean
	public MultiGridFsTemplate multiGridFsTemplate(MultiMongoDatabaseFactory databaseFactory,
			MongoTemplate mongoTemplate) {

		Map<String, MongoDatabaseFactory> factoryMap = databaseFactory.getDatabaseFactory();

		Map<String, GridFsMongoDatabaseFactory> data = new HashMap<>();

		factoryMap.forEach((key, factory) -> {
			GridFsMongoDatabaseFactory extend = new GridFsMongoDatabaseFactory(factory, multi.get(key));
			data.put(key, extend);
		});

		MultiGridFsDatabaseFactory gridFsDatabaseFactory = new MultiGridFsDatabaseFactory(data, primary);
		MultiGridFsTemplate gridFsTemplate = new MultiGridFsTemplate(gridFsDatabaseFactory,
				mongoTemplate.getConverter());
		return gridFsTemplate;
	}
}
